查看原文
其他

已投入生产的Android动态切换应用图标方案

技术最TOP 2022-08-26

作者:下午吃早餐同学链接:https://juejin.cn/post/7018100275336462372

动态切换应用图标

注意:我公司已采用此方案投入生产,此方案存在缺陷并非完美方案,采用前请评估是否接受缺陷,具体缺陷见文末。

效果图

产品需求

市面上很多App能根据特定活动,动态切换应用图标达到宣传目的,例如淘宝双十一,国庆节等等。那么我们怎样才能在不发新版本的情况下,动态切换应用图标呢?

具体方案

  • 1.图标更换:在AndroidManifest设置应用入口Activity的别名,然后通过setComponentEnabledSetting动态启用或禁用别名进行图标切换
  • 2.控制图标显示:冷启动App时,调用接口判断是否需要切换icon ( 3.触发时机:监听App前后台切换,当App处于后台时切换图标,使得用户无感知。

代码实现

AndroidManifest.xml中给入口Activity设置activity-alias

    <application
        android:name=".MyApplication"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/Theme.SwitchIcon">


        <!-- 原MainActivity -->
        <activity android:name=".MainActivity" />

        <!-- 固定设置一个默认的别名,用来替代原MainActivity -->
        <activity-alias
            android:name=".DefaultAliasActivity"
            android:enabled="true"
            android:icon="@mipmap/ic_launcher"
            android:label="@string/app_name"
            android:targetActivity=".MainActivity">

            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity-alias>


        <!-- 别名1,特定活动需要的图标如:双11,国庆节等 -->
        <activity-alias
            android:name=".Alias1Activity"
            android:enabled="false"
            android:icon="@mipmap/ic_launcher_show"
            android:label="@string/app_name"
            android:targetActivity=".MainActivity">

            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity-alias>
        
    </application>

activity-alias标签中的属性如下

标签作用android:name别名,命名规则同Activelyandroid:enabled是否启用别名,这里的主要作用的控制显示应用图标android:icon应用图标android:label应用名android:targetActivity必须指向原入口Activity

在MainActivity中,通过启用或禁用别名进行图标切换

/**
 * 设置默认的别名为启动入口
 */

public void setDefaultAlias() {
    PackageManager packageManager = getPackageManager();
    
    ComponentName name1 = new ComponentName(this"com.fengfeibiao.switchicon.DefaultAliasActivity");
    packageManager.setComponentEnabledSetting(name1, PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP);
    
    ComponentName name2 = new ComponentName(this"com.fengfeibiao.switchicon.Alias1Activity");
    packageManager.setComponentEnabledSetting(name2, PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP);
    }

/**
 * 设置别名1为启动入口
 */

public void setAlias1() {
    PackageManager packageManager = getPackageManager();
    ComponentName name1 = new ComponentName(this"com.fengfeibiao.switchicon.DefaultAliasActivity");
    packageManager.setComponentEnabledSetting(name1, PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP);

    ComponentName name2 = new ComponentName(this"com.fengfeibiao.switchicon.Alias1Activity");
    packageManager.setComponentEnabledSetting(name2, PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP);
}

ForegroundCallbacks监听App前后台切换

/**
 * 监听App前后台切换
 */

public class ForegroundCallbacks implements Application.ActivityLifecycleCallbacks {
    public static final long CHECK_DELAY = 500;
    public static final String TAG = ForegroundCallbacks.class.getName();

    public interface Listener {
        void onForeground();

        void onBackground();
    }

    private static ForegroundCallbacks instance;
    private boolean foreground = false, paused = true;
    private Handler handler = new Handler();
    private List<Listener> listeners = new CopyOnWriteArrayList<Listener>();
    private Runnable check;

    public static ForegroundCallbacks init(Application application) {
        if (instance == null) {
            instance = new ForegroundCallbacks();
            application.registerActivityLifecycleCallbacks(instance);
        }
        return instance;
    }

    public static ForegroundCallbacks get(Application application) {
        if (instance == null) {
            init(application);
        }
        return instance;
    }

    public static ForegroundCallbacks get(Context ctx) {
        if (instance == null) {
            Context appCtx = ctx.getApplicationContext();
            if (appCtx instanceof Application) {
                init((Application) appCtx);
            }
            throw new IllegalStateException(
                    "Foreground is not initialised and " +
                            "cannot obtain the Application object");
        }
        return instance;
    }

    public static ForegroundCallbacks get() {
        if (instance == null) {
            throw new IllegalStateException(
                    "Foreground is not initialised - invoke " +
                            "at least once with parameterised init/get");
        }
        return instance;
    }

    public boolean isForeground() {
        return foreground;
    }

    public boolean isBackground() {
        return !foreground;
    }

    public void addListener(Listener listener) {
        listeners.add(listener);
    }

    public void removeListener(Listener listener) {
        listeners.remove(listener);
    }

    @Override
    public void onActivityResumed(Activity activity) {
        paused = false;
        boolean wasBackground = !foreground;
        foreground = true;
        if (check != null)
            handler.removeCallbacks(check);
        if (wasBackground) {
            Log.d(TAG, "went foreground");

            for (Listener l : listeners) {
                try {
                    l.onForeground();


                } catch (Exception exc) {
                    Log.d(TAG, "Listener threw exception!:" + exc.toString());
                }
            }
        } else {
            Log.d(TAG, "still foreground");
        }
    }

    @Override
    public void onActivityPaused(Activity activity) {
        paused = true;
        if (check != null)
            handler.removeCallbacks(check);
        handler.postDelayed(check = new Runnable() {
            @Override
            public void run() {
                if (foreground && paused) {
                    foreground = false;
                    Log.d(TAG, "went background");
                    for (Listener l : listeners) {
                        try {
                            l.onBackground();
                        } catch (Exception exc) {
                            Log.d(TAG, "Listener threw exception!:" + exc.toString());
                        }
                    }
                } else {
                    Log.d(TAG, "still foreground");
                }
            }
        }, CHECK_DELAY);
    }

    @Override
    public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
    }

    @Override
    public void onActivityStarted(Activity activity) {
    }

    @Override
    public void onActivityStopped(Activity activity) {
    }

    @Override
    public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
    }

    @Override
    public void onActivityDestroyed(Activity activity) {
    }
}

需要在Application中调用ForegroundCallbacks.init(this)进行初始化 在MainActivity中实现ForegroundCallbacks.Listener对App进行监听,当处于后台判断是否切换应用图标

完整的MainActivity代码:

public class MainActivity extends AppCompatActivity implements ForegroundCallbacks.Listener {

    private int position = 0;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //添加app前后台监听
        ForegroundCallbacks.get(this).addListener(this);

        findViewById(R.id.tv_default).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                position = 0;
            }
        });

        findViewById(R.id.tv_alias1).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                position = 1;
            }
        });

    }

    @Override
    protected void onDestroy() {
        // 移除app前后台监听
        ForegroundCallbacks.get(this).removeListener(this);
        super.onDestroy();
    }

    @Override
    public void onForeground() {

    }

    @Override
    public void onBackground() {
        //根据具体业务需求设置切换条件,我公司采用接口控制icon切换
        if (position == 0) {
            setDefaultAlias();
        } else {
            setAlias1();
        }
    }


    /**
     * 设置默认的别名为启动入口
     */

    public void setDefaultAlias() {
        PackageManager packageManager = getPackageManager();

        ComponentName name1 = new ComponentName(this"com.fengfeibiao.switchicon.DefaultAliasActivity");
        packageManager.setComponentEnabledSetting(name1, PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP);

        ComponentName name2 = new ComponentName(this"com.fengfeibiao.switchicon.Alias1Activity");
        packageManager.setComponentEnabledSetting(name2, PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP);
    }

    /**
     * 设置别名1为启动入口
     */

    public void setAlias1() {
        PackageManager packageManager = getPackageManager();

        ComponentName name1 = new ComponentName(this"com.fengfeibiao.switchicon.DefaultAliasActivity");
        packageManager.setComponentEnabledSetting(name1, PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP);

        ComponentName name2 = new ComponentName(this"com.fengfeibiao.switchicon.Alias1Activity");
        packageManager.setComponentEnabledSetting(name2, PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP);
    }
}

具体缺陷如下:

  • 切换icon会关闭应用进程,不是崩溃所以不会上报bugly
  • 切换icon需要时间,部分华为机型要10s左右,之后能正常打开
  • 切换icon过程中,部分机型点击图标无法打开应用,提示应用未安装

2021.11.15更新

魅族机型 16S 不能动态切换icon, 我公司已采用本方案投入生产,后续发现有缺陷及解决办法会及时更新。如采用本方案有其他问题,欢迎留言。

Demo的github地址 https://github.com/FengFeiBiao/SwitchIcon

参考文章 【监听App进入前后台】:https://blog.csdn.net/mapboo/article/details/104073789



---END---


推荐阅读:
写给Android工程师的AOP知识!
为何推荐在自定义View中抽Drawable!
Kotlin flow实践总结!
来了,Android 13首个开发者预览版!
Android自定义View实现图形验证~
Jetpack—LiveData组件的缺陷以及应对策略!
2021年总结,断更大半年,回了重庆,进了大厂!
用更优雅的技术方案实现应用内多弹窗效果!
安装一个 apk 引起的无法开机!

更文不易,点个“在看”支持一下👇

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存